package com.jjoe64.graphview;

import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

import com.jjoe64.graphview.compatible.ScaleGestureDetector;

/**
 * GraphView is a Android View for creating zoomable and scrollable graphs. This
 * is the abstract base class for all graphs. Extend this class and implement
 * {@link #drawSeries(Canvas, GraphViewData[], float, float, float, double, double, double, double, float)}
 * to display a custom graph. Use {@link LineGraphView} for creating a line
 * chart.
 * 
 * @author jjoe64 - jonas gehring - http://www.jjoe64.com
 * 
 *         Copyright (C) 2011 Jonas Gehring Licensed under the GNU Lesser
 *         General Public License (LGPL) http://www.gnu.org/licenses/lgpl.html
 */
abstract public class GraphView extends LinearLayout {
	static final private class GraphViewConfig {
		static final float BORDER = 20;
		static final float VERTICAL_LABEL_HEIGHT = 60;
		static final float HORIZONTAL_LABEL_WIDTH = 100;
	}

	private class GraphViewContentView extends View {
		private float lastTouchEventX, lastTouchEventY;
		private float graphwidth;

		private SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
				"MM-dd-yyyy");
		private String currentDate;

		/**
		 * @param context
		 */
		@SuppressWarnings("deprecation")
		public GraphViewContentView(Context context) {
			super(context);
			setLayoutParams(new LayoutParams(
					android.view.ViewGroup.LayoutParams.FILL_PARENT,
					android.view.ViewGroup.LayoutParams.FILL_PARENT));
		}

		/**
		 * @param canvas
		 */
		@Override
		protected void onDraw(Canvas canvas) {

			paint.setAntiAlias(true);

			// normal
			paint.setStrokeWidth(0);

			float border = GraphViewConfig.BORDER;
			float horstart = 0;
			float height = getHeight();
			float width = getWidth() - 1;
			double maxY = getMaxY();
			double minY = getMinY();
			double diffY = maxY - minY;
			double maxX = getMaxX(false);
			double minX = getMinX(false);
			double diffX = maxX - minX;
			float graphheight = height - (2 * border);
			graphwidth = width;

			if (verlabels == null) {
				verlabels = generateVerlabels(graphheight);
			}

			// vertical lines

			paint.setTextAlign(Align.LEFT);
			int vers = verlabels.length - 1;

			for (int i = 0; i < verlabels.length; i++) {
				paint.setColor(Color.LTGRAY);
				float y = ((graphheight / vers) * i) + border;
				canvas.drawLine(horstart, y, width, y, paint);
			}

			if (horlabels == null || acceptsLiveData())
				horlabels = generateHorlabels(graphwidth);
			// horizontal labels + lines
			int hors = horlabels.length - 1;
			for (int i = 0; i < horlabels.length; i++) {
				paint.setColor(Color.LTGRAY);
				float x = ((graphwidth / hors) * i) + horstart;
				canvas.drawLine(x, height - border, x, border, paint);

				// Labels
				paint.setTextAlign(Align.CENTER);
				paint.setTextSize(12.0f);
				if (i == horlabels.length - 1)
					paint.setTextAlign(Align.RIGHT);
				if (i == 0)
					paint.setTextAlign(Align.LEFT);
				paint.setColor(Color.WHITE);
				canvas.drawText(horlabels[i], x, height - 4, paint);
			}
			// draw title
			paint.setTextAlign(Align.CENTER);
			paint.setTextSize(18.0f);

			currentDate = simpleDateFormat.format(Calendar.getInstance()
					.getTime());
			paint.setColor(Color.WHITE);
			paint.setTypeface(Typeface.DEFAULT_BOLD);
			canvas.drawText(title + " - " + currentDate, (graphwidth / 2)
					+ horstart, border - 4, paint);

			if (maxY != minY) {
				paint.setStrokeCap(Paint.Cap.ROUND);

				for (int i = 0; i < graphSeries.size(); i++) {
					try {
						paint.setStrokeWidth(graphSeries.get(i).style.thickness);
						paint.setColor(graphSeries.get(i).style.color);
						drawSeries(canvas, _values(i), graphwidth, graphheight,
								border, minX, minY, diffX, diffY, horstart);
					} catch (NullPointerException e) {
						e.printStackTrace();

					}
				}

				if (showLegend)
					drawLegend(canvas, height, width);
			}
		}

		private void onMoveGesture(float f, boolean isHorizontalAxis) {
			if (!isHorizontalAxis) {
				int zoomIndex = (int) (Math.log(getMaxY() - getMinY()) / Math
						.log(2));
				double offset = f * Math.pow(2, zoomIndex - 9);
				setManualYAxisBounds(getMaxY() + offset, getMinY() + offset);
				regenerateVerLabels();
				invalidate();
				return;
			}
			// view port update
			if (viewportSize != 0) {
				viewportStart -= f * viewportSize / graphwidth;

				// minimal and maximal view limit
				double minX = getMinX(true);
				double maxX = getMaxX(true);
				if (viewportStart < minX) {
					viewportStart = minX;
				} else if (viewportStart + viewportSize > maxX) {
					viewportStart = maxX - viewportSize;
				}

				// labels have to be regenerated
				horlabels = null;

				// If autoScale is set, regenerate vertical Labels
				if (isVerticalAxisAutoScale)
					regenerateVerLabels();
			}
			invalidate();
		}

		/**
		 * @param event
		 */
		@Override
		public boolean onTouchEvent(MotionEvent event) {
			if (!isScrollable()) {
				return super.onTouchEvent(event);
			}

			boolean handled = false;
			// first scale
			if (scalable && scaleDetector != null) {
				scaleDetector.onTouchEvent(event);
				handled = scaleDetector.isInProgress();
			}
			if (!handled) {
				// if not scaled, scroll
				if ((event.getAction() & MotionEvent.ACTION_DOWN) == MotionEvent.ACTION_DOWN) {

					handled = true;
				}
				if ((event.getAction() & MotionEvent.ACTION_UP) == MotionEvent.ACTION_UP) {
					lastTouchEventX = 0;
					lastTouchEventY = 0;
					handled = true;
				}
				if ((event.getAction() & MotionEvent.ACTION_MOVE) == MotionEvent.ACTION_MOVE) {
					if (lastTouchEventX != 0) {
						onMoveGesture(event.getX() - lastTouchEventX, true);
					}
					if (lastTouchEventY != 0) {
						onMoveGesture(event.getY() - lastTouchEventY, false);
					}
					lastTouchEventX = event.getX();
					lastTouchEventY = event.getY();
					handled = true;
				}
				if (handled)
					invalidate();
			}
			return handled;
		}
	}

	/**
	 * graph series style: color and thickness
	 */
	static public class GraphViewStyle {
		public int color = 0xff0077cc;
		public int thickness = 3;

		public GraphViewStyle() {
			super();
		}

		public int getColor() {
			return color;
		}

		public void setColor(int color) {
			this.color = color;
		}

		public int getThickness() {
			return thickness;
		}

		public void setThickness(int thickness) {
			this.thickness = thickness;
		}

		public GraphViewStyle(int color, int thickness) {
			super();
			this.color = color;
			this.thickness = thickness;
		}
	}

	/**
	 * one data set for a graph series
	 */
	static public class GraphViewData {
		public final double valueX;
		public final double valueY;

		public GraphViewData(double valueX, double valueY) {
			super();
			this.valueX = valueX;
			this.valueY = valueY;
		}
	}

	/**
	 * a graph series
	 */
	static public class GraphViewSeries {
		final String description;
		final GraphViewStyle style;
		List<GraphViewData> values;

		public GraphViewSeries(List<GraphViewData> values) {
			description = null;
			style = new GraphViewStyle();
			this.values = Collections
					.synchronizedList(new ArrayList<GraphViewData>());
			this.values.addAll(values);
		}

		public GraphViewSeries(String description, GraphViewStyle style,
				List<GraphViewData> values) {
			super();
			this.description = description;
			if (style == null) {
				style = new GraphViewStyle();
			}
			this.style = style;
			this.values = Collections
					.synchronizedList(new ArrayList<GraphViewData>());
			this.values.addAll(values);
		}

		public void addData(GraphViewData data) {
			this.values.add(data);
			
		}

		public void resetData(List<GraphViewData> data) {
			this.values = data;
		}

		public List<GraphViewData> getDataList() {
			return this.values;
		}
	}

	public enum LegendAlign {
		TOP, MIDDLE, BOTTOM
	}

	private class VerLabelsView extends View {
		/**
		 * @param context
		 */
		public VerLabelsView(Context context) {
			super(context);
			setLayoutParams(new LayoutParams(
					android.view.ViewGroup.LayoutParams.FILL_PARENT,
					android.view.ViewGroup.LayoutParams.FILL_PARENT, 10));
		}

		/**
		 * @param canvas
		 */
		@Override
		protected void onDraw(Canvas canvas) {
			// normal
			paint.setStrokeWidth(0);

			float border = GraphViewConfig.BORDER;
			float height = getHeight();
			float graphheight = height - (2 * border);

			if (verlabels == null) {
				verlabels = generateVerlabels(graphheight);
			}

			// vertical labels
			paint.setTextAlign(Align.LEFT);
			paint.setTextSize(12.0f);

			int vers = verlabels.length - 1;
			for (int i = 0; i < verlabels.length; i++) {
				float y = ((graphheight / vers) * i) + border;
				paint.setColor(Color.WHITE);
				canvas.drawText(verlabels[i], 0, y, paint);
			}
		}
	}

	protected final Paint paint;
	private String[] horlabels;
	private String[] verlabels;
	private String title;
	private boolean scrollable;
	private double viewportStart;
	private double viewportSize;
	private final View viewVerLabels;
	private ScaleGestureDetector scaleDetector;
	private boolean scalable;
	private NumberFormat numberformatter;
	private final List<GraphViewSeries> graphSeries;
	private boolean showLegend = false;
	private float legendWidth = 120;
	private LegendAlign legendAlign = LegendAlign.MIDDLE;
	private boolean manualYAxis;
	private double manualMaxYValue;
	private double manualMinYValue;
	private boolean isVerticalAxisAutoScale = false;
	private boolean acceptsLiveData;

	/**
	 * 
	 * @param context
	 * @param title
	 *            [optional]
	 */
	public GraphView(Context context, String title) {
		super(context);
		setLayoutParams(new LayoutParams(
				android.view.ViewGroup.LayoutParams.FILL_PARENT,
				android.view.ViewGroup.LayoutParams.FILL_PARENT));

		if (title == null)
			title = "";
		else
			this.title = title;

		paint = new Paint();
		graphSeries = new ArrayList<GraphViewSeries>();

		viewVerLabels = new VerLabelsView(context);
		addView(viewVerLabels);
		addView(new GraphViewContentView(context), new LayoutParams(
				android.view.ViewGroup.LayoutParams.FILL_PARENT,
				android.view.ViewGroup.LayoutParams.FILL_PARENT, 1));
	}

	private List<GraphViewData> _values(int idxSeries) {
		List<GraphViewData> values = graphSeries.get(idxSeries).values;
		if (viewportStart == 0 && viewportSize == 0) {
			// all data
			return values;
		} else {
			// viewport
			List<GraphViewData> listData = new ArrayList<GraphViewData>();
			for (int i = 0; i < values.size(); i++) {
				if (values.get(i).valueX >= viewportStart) {
					if (values.get(i).valueX > viewportStart + viewportSize) {
						listData.add(values.get(i)); // one more for nice
														// scrolling
						break;
					} else {
						listData.add(values.get(i));
					}
				} else {
					if (listData.isEmpty()) {
						listData.add(values.get(i));
					}
					listData.set(0, values.get(i)); // one before, for nice
					// scrolling
				}
			}
			return listData;
		}
	}

	public void addSeries(GraphViewSeries series) {
		if (!hasSeries(series))
			graphSeries.add(series);
	}

	public void removeSeries(int index) {
		if (index < 0 || index >= graphSeries.size()) {
			throw new IndexOutOfBoundsException("No series at index " + index);
		}

		graphSeries.remove(index);
	}

	public void removeSeries(GraphViewSeries series) {
		if (hasSeries(series))
			graphSeries.remove(series);
	}

	public boolean hasAnySeries() {
		return graphSeries.size() > 0;

	}

	public boolean hasSeries(GraphViewSeries series) {

		return graphSeries.contains(series);
	}

	protected void drawLegend(Canvas canvas, float height, float width) {
		int shapeSize = 15;
		if (graphSeries.size() == 0)
			return;
		// rect
		paint.setARGB(180, 100, 100, 100);
		float legendHeight = (shapeSize + 5) * graphSeries.size() + 5;
		float lLeft = width - legendWidth - 10;
		float lTop;
		switch (legendAlign) {
		case TOP:
			lTop = 10;
			break;
		case MIDDLE:
			lTop = height / 2 - legendHeight / 2;
			break;
		default:
			lTop = height - GraphViewConfig.BORDER - legendHeight - 10;
		}
		float lRight = lLeft + legendWidth;
		float lBottom = lTop + legendHeight;
		canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8,
				paint);

		for (int i = 0; i < graphSeries.size(); i++) {
			paint.setColor(graphSeries.get(i).style.color);
			canvas.drawRect(new RectF(lLeft + 5, lTop + 5
					+ (i * (shapeSize + 5)), lLeft + 5 + shapeSize, lTop
					+ ((i + 1) * (shapeSize + 5))), paint);
			if (graphSeries.get(i).description != null) {
				paint.setColor(Color.WHITE);
				paint.setTextSize(14f);
				paint.setTextAlign(Align.LEFT);
				canvas.drawText(graphSeries.get(i).description, lLeft + 5
						+ shapeSize + 5, lTop + shapeSize
						+ (i * (shapeSize + 5)), paint);
			}
		}
	}

	abstract public void drawSeries(Canvas canvas, List<GraphViewData> list,
			float graphwidth, float graphheight, float border, double minX,
			double minY, double diffX, double diffY, float horstart);

	/**
	 * formats the label can be overwritten
	 * 
	 * @param value
	 *            x and y values
	 * @param isValueX
	 *            if false, value y wants to be formatted
	 * @return value to display
	 */
	protected String formatLabel(double value, boolean isValueX) {

		if (numberformatter == null) {
			numberformatter = NumberFormat.getNumberInstance();
			double highestvalue = getMaxY();
			double lowestvalue = getMinY();
			if (highestvalue - lowestvalue < 0.1) {
				numberformatter.setMaximumFractionDigits(6);
			} else if (highestvalue - lowestvalue < 1) {
				numberformatter.setMaximumFractionDigits(4);
			} else if (highestvalue - lowestvalue < 20) {
				numberformatter.setMaximumFractionDigits(3);
			} else if (highestvalue - lowestvalue < 100) {
				numberformatter.setMaximumFractionDigits(1);
			} else {
				numberformatter.setMaximumFractionDigits(0);
			}
		}
		return numberformatter.format(value);
	}

	private String[] generateHorlabels(float graphwidth) {
		int numLabels = (int) (graphwidth / GraphViewConfig.HORIZONTAL_LABEL_WIDTH);
		String[] labels = new String[numLabels + 1];
		double min = getMinX(false);
		double max = getMaxX(false);
		for (int i = 0; i <= numLabels; i++) {
			labels[i] = formatLabel(min + ((max - min) * i / numLabels), true);
		}
		return labels;
	}

	public void regenerateVerLabels() {

		verlabels = null;
		viewVerLabels.invalidate();
	}

	synchronized private String[] generateVerlabels(float graphheight) {
		int numLabels = (int) (graphheight / GraphViewConfig.VERTICAL_LABEL_HEIGHT);
		String[] labels = new String[numLabels + 1];
		double min = getMinY();
		double max = getMaxY();
		for (int i = 0; i <= numLabels; i++) {
			labels[numLabels - i] = formatLabel(min
					+ ((max - min) * i / numLabels), false);
		}
		return labels;
	}

	public LegendAlign getLegendAlign() {
		return legendAlign;
	}

	public float getLegendWidth() {
		return legendWidth;
	}

	/**
	 * returns the maximal X value of the current viewport (if viewport is set)
	 * otherwise maximal X value of all data.
	 * 
	 * @param ignoreViewport
	 * 
	 *            warning: only override this, if you really know want you're
	 *            doing!
	 */
	public double getMaxX(boolean ignoreViewport) {
		// if viewport is set, use this
		if (!ignoreViewport && viewportSize != 0) {
			return viewportStart + viewportSize;
		} else {
			// otherwise use the max x value
			// values must be sorted by x, so the last value has the largest X
			// value
			double highest = 0;

			if (graphSeries.size() > 0) {
				List<GraphViewData> values = graphSeries.get(0).values;
				// highest=values.get(values.size()-1).valueX;

				try {
					highest = Collections.max(values,
							new Comparator<GraphViewData>() {

								public int compare(GraphViewData a,
										GraphViewData b) {

									if (a.valueX > b.valueX)
										return 1;
									else if (b.valueX > a.valueX)
										return -1;
									else
										return 0;
								}
							}).valueX;
				} catch (NullPointerException e) {
					e.printStackTrace();
				}

			}
			
			return highest;
		}
	}

	/**
	 * returns the maximal Y value of all data.
	 * 
	 * warning: only override this, if you really know want you're doing!
	 */
	public double getMaxY() {
		double largest;
		double temp;
		if (manualYAxis) {
			largest = manualMaxYValue;
		} else {
			largest = Double.MIN_VALUE;
			for (int i = 0; i < graphSeries.size(); i++) {
				List<GraphViewData> values = _values(i);
				temp = Collections.max(values, new Comparator<GraphViewData>() {

					public int compare(GraphViewData a, GraphViewData b) {
						// TODO Auto-generated method stub
						if (a.valueY > b.valueY)
							return 1;
						else if (b.valueY > a.valueY)
							return -1;
						else
							return 0;
					}
				}).valueY;
				if (temp > largest)
					largest = temp;
				/*
				 * for (int ii = 0; ii < values.size(); ii++) if
				 * (values.get(ii).valueY > largest) largest =
				 * values.get(ii).valueY;
				 */
			}
		}
		
		return largest;
	}

	/**
	 * returns the minimal X value of the current viewport (if viewport is set)
	 * otherwise minimal X value of all data.
	 * 
	 * @param ignoreViewport
	 * 
	 *            warning: only override this, if you really know want you're
	 *            doing!
	 */
	public double getMinX(boolean ignoreViewport) {
		// if viewport is set, use this
		if (!ignoreViewport && viewportSize != 0) {
			return viewportStart;
		} else {
			// otherwise use the min x value
			// values must be sorted by x, so the first value has the smallest X
			// value
			double lowest = 0;

			if (graphSeries.size() > 0) {
				List<GraphViewData> values = graphSeries.get(0).values;
				// lowest=values.get(0).valueX;
				lowest = Collections.min(values,
						new Comparator<GraphViewData>() {

							public int compare(GraphViewData a, GraphViewData b) {

								if (a.valueX > b.valueX)
									return 1;
								else if (b.valueX > a.valueX)
									return -1;
								else
									return 0;
							}
						}).valueX;
				/*
				 * lowest = values.get(0).valueX;
				 * 
				 * for (int i = 1; i < graphSeries.size(); i++) { values =
				 * graphSeries.get(i).values; lowest = Math.min(lowest,
				 * values.get(0).valueX); }
				 */
			}
			
			return lowest;
		}
	}

	/**
	 * returns the minimal Y value of all data.
	 * 
	 * warning: only override this, if you really know want you're doing!
	 */
	public double getMinY() {
		double smallest, temp;
		if (manualYAxis) {
			smallest = manualMinYValue;
		} else {
			smallest = Double.MAX_VALUE;
			for (int i = 0; i < graphSeries.size(); i++) {
				List<GraphViewData> values = _values(i);
				temp = Collections.min(values, new Comparator<GraphViewData>() {

					public int compare(GraphViewData a, GraphViewData b) {
						// TODO Auto-generated method stub
						if (a.valueY > b.valueY)
							return 1;
						else if (b.valueY > a.valueY)
							return -1;
						else
							return 0;
					}
				}).valueY;
				smallest = Math.min(smallest, temp);
				/*
				 * for (int ii = 0; ii < values.size(); ii++) if
				 * (values.get(ii).valueY > smallest) smallest =
				 * values.get(ii).valueY;
				 */
			}
		}
		
		return smallest;
	}

	public boolean isScrollable() {
		return scrollable;
	}

	public boolean isShowLegend() {
		return showLegend;
	}

	/**
	 * set's static horizontal labels (from left to right)
	 * 
	 * @param horlabels
	 *            if null, labels were generated automatically
	 */
	public void setHorizontalLabels(String[] horlabels) {
		this.horlabels = horlabels;
	}

	public void setLegendAlign(LegendAlign legendAlign) {
		this.legendAlign = legendAlign;
	}

	public void setLegendWidth(float legendWidth) {
		this.legendWidth = legendWidth;
	}

	/**
	 * you have to set the bounds {@link #setManualYAxisBounds(double, double)}.
	 * That automatically enables manualYAxis-flag. if you want to disable the
	 * menual y axis, call this method with false.
	 * 
	 * @param manualYAxis
	 */
	public void setManualYAxis(boolean manualYAxis) {
		this.manualYAxis = manualYAxis;
	}

	/**
	 * set manual Y axis limit
	 * 
	 * @param max
	 * @param min
	 */
	public void setManualYAxisBounds(double max, double min) {
		manualMaxYValue = max;
		manualMinYValue = min;
		manualYAxis = true;
	}

	/**
	 * this forces scrollable = true
	 * 
	 * @param scalable
	 */
	synchronized public void setScalable(boolean scalable) {
		this.scalable = scalable;
		if (scalable == true && scaleDetector == null) {
			scrollable = true; // automatically forces this
			scaleDetector = new ScaleGestureDetector(getContext(),
					new ScaleGestureDetector.SimpleOnScaleGestureListener() {

						public boolean onScale(ScaleGestureDetector detector) {

							double center = viewportStart + viewportSize / 2;
							viewportSize /= detector.getScaleFactor();
							viewportStart = center - viewportSize / 2;

							// viewportStart must not be < minX
							double minX = getMinX(true);
							if (viewportStart < minX) {
								viewportStart = minX;
							}

							// viewportStart + viewportSize must not be > maxX
							double maxX = getMaxX(true);
							double overlap = viewportStart + viewportSize
									- maxX;
							if (overlap > 0) {
								// scroll left
								if (viewportStart - overlap > minX) {
									viewportStart -= overlap;
								} else {
									// maximal scale
									viewportStart = minX;
									viewportSize = maxX - viewportStart;
								}
							}

							verlabels = null;
							horlabels = null;
							numberformatter = null;
							invalidate();
							viewVerLabels.invalidate();
							return true;
						}

					});
		}
	}

	/**
	 * the user can scroll (horizontal) the graph. This is only useful if you
	 * use a viewport {@link #setViewPort(double, double)} which doesn't
	 * displays all data.
	 * 
	 * @param scrollable
	 */

	public void setScrollable(boolean scrollable) {
		this.scrollable = scrollable;
	}

	public void setShowLegend(boolean showLegend) {
		this.showLegend = showLegend;
	}

	public void setAcceptsLiveData(boolean value) {
		this.acceptsLiveData = value;
	}

	public boolean acceptsLiveData() {
		return this.acceptsLiveData;
	}

	public void setVerticalAxisAutoScale(boolean isAutoScale) {
		setManualYAxis(!isAutoScale);
		this.isVerticalAxisAutoScale = isAutoScale;
	}

	public boolean isVerticalAxisAutoScale() {
		return this.isVerticalAxisAutoScale;
	}

	/**
	 * set's static vertical labels (from top to bottom)
	 * 
	 * @param verlabels
	 *            if null, labels were generated automatically
	 */
	public void setVerticalLabels(String[] verlabels) {
		this.verlabels = verlabels;
	}

	/**
	 * set's the viewport for the graph.
	 * 
	 * @param start
	 *            x-value
	 * @param size
	 */
	public void setViewPort(double start, double size) {
		viewportStart = start;
		viewportSize = size;
	}
}
